package com.text
{
    import flash.display.BitmapData;
    import flash.events.Event;
    import flash.geom.Matrix;
    import flash.geom.Rectangle;
    import flash.text.TextField;
    
    import mx.controls.textClasses.TextRange;
    import mx.core.UIComponent;
    import mx.utils.UIDUtil;
 
    
    internal class Highlighter
    {   
    	/**
    	* The TextField being highlighted.
    	*/
        private var textfield:TextField;
      
      	/**
      	 * Textcontrol to operate on
      	 */ 
        private var target:CustomTextArea;
        
        
        public var textFormat:CustomTextFormat;
         
        /**
        * The horizontal offset for highlights.
        * 
        * @default 0
        */
        public var xOffset:Number;
        
        /**
        * The vertical offset for highlights.
        * 
        * @default 0
        */
        public var yOffset:Number;
        /***
        * underline pattern used to highlight. propably this should be outside with its generation function
        */
        private var _underlinePattern:BitmapData = null;
        /**
        * Contains the bounding rectangles of each string to be highlighted.
        */
        private var boundariesToHighlight:Array = [];
          
 	 	/** use <A href tags around selections .**/
 		private var useTagging:Boolean;
 		
 		/** array of words highlighted if highlighing is by words **/
 		private var words:Array = null ;
 
 		 /** if word search is casesensitive **/
        private var caseSensitive:Boolean = false;
 		 	 
 		private var id:String = UIDUtil.createUID();
 		private var attachedEvents :Boolean = false;
 		
 		/**key uniquely identifies this highlighter **/
 		private var _key:String = UIDUtil.createUID();
 		public function get key():String
 		{
 			return _key ;
 		}
 		public function set key(value:String):void
 		{
 		  if (value !=null && value.length > 0 )
 		     _key = value;
 		  else
 		     _key = UIDUtil.createUID();
 		} 		
 		
 		 /**returns an array of Ranges of currently selected items*/
        public function get ranges():Array
        {
        	return this.boundariesToHighlight;
        }
 		
        /**
        * Finds & highlights strings in a text textfield.
        * 
        * @param target The Target textfield containing the text to highlight
        * @param textformat CustomTextFormat contain
        * @param xOffset If necessary, the horizontal offset of the highlight.  Useful when the TextField has some padding applied to it.  Default is 0.
        * @param yOffset If necessary, the vertical offset of the highlight.  Useful when the TextField has some padding applied to it.  Default is 0.
        */
        public function Highlighter(target:CustomTextArea,textfield:TextField,
         textFormat:CustomTextFormat,key:String=null,xOffset:Number=0,yOffset:Number=0)
        {
            this.key = key;
            this.xOffset = xOffset;
            this.yOffset = yOffset; 
          	this.textFormat = textFormat;
            this.textfield = textfield;
            this.target = target;
            this.boundariesToHighlight = new Array();   
            
        }
      
		/**
		* Highlights all instances of a string.
		* @param word The string to find and highlight.
		*/
        public function highlightWord(word:String,caseSensitive:Boolean=false):void
        {  
           this.highlightWords(new Array(word),caseSensitive);
        }
        
        /**
        * Highlights all words in the array , useful for spelling checker for example
        */
        public function highlightWords(words:Array,caseSensitive:Boolean=false):void
        {   
        	this.words = words;
        	this.useTagging = false;
        	this.caseSensitive = caseSensitive;
			locateWords(); //generate the word positions array
			listen();
			target.invalidateHighlights();
		}
		
        /**
        * Highlights all indexes and ranges in the array passed 
        * a range begins at zero based index and ends at one based index 
        * eg. the 1st character in text range is (0,1) which is consistent with double index parameter functions. 
        * eg. var a : Array = new Array();
			var delim : Object = new Object();
			delim.start = 3;
			delim.end = 10;
			a.push( delim );
			highlighter.highlightWords( a );
		* It is preferrable that ranges do not intersect.
        **/
        public function highlightRanges(indexes: Array ) : void
		{
			if (indexes == null) return;
			useTagging = true;
			var len:int = this.textfield.text.length;//zero based begin index , 1 based end index
			for(var i : int = 0; i < indexes.length; i++)
			{   if (Range(indexes[i]).beginIndex == Range(indexes[i]).endIndex || Range(indexes[i]).endIndex > len ) continue;
				var str : StringBoundary = new StringBoundary( this.textfield, Range(indexes[i]).beginIndex, Range(indexes[i]).endIndex, this.xOffset, this.yOffset);
				this.boundariesToHighlight.push( str );
			}
			//insert tags around the boundaries
			tagBoundaries(); 
		 	//locateTags();
			listen();//listen for redrawas
			target.invalidateHighlights();
		}
        
        /** Highlights a single range in the array passed 
        * from zero based beginindex to one based end index*/ 
        public function highlightRange(beginIndex:int, endIndex:int):void
        {
            var index:Object = new Object();
            index.begin = beginIndex;
            index.end = endIndex;
            highlightRanges(new Array(index)); 
        }
        
     
 
       /***
       *  returns the next highlighted range or null if its not circular and we reached the end of the boundaries
       *  if reached the end and circular is true then wejump to the first highlighted word 
       *  
       */  
        public function getNext(circular:Boolean=false,startIndex:int=-1):Range
        {    
        	 var boundary:StringBoundary ;
             var len:int = this.boundariesToHighlight.length ;
             var caretIndex:int = (startIndex==-1 ?  this.textfield.caretIndex : startIndex) ;
              //search for first boundary from caretIndex
              for ( var i:int= 0 ; i<len; i++)
	          {
                 boundary = StringBoundary(this.boundariesToHighlight[i]);
                 if (boundary.beginIndex >= caretIndex)
                    break; 
	          }
              if (i==len && circular)
	          {  
				 boundary = StringBoundary(this.boundariesToHighlight[0]);
	          }
	          else if (i==len)
	             return null;
             return new Range(boundary.beginIndex,boundary.endIndex);
        }  
        
         /***
       *  returns the prev highlighted range or null if its not circular and we reached the end of the boundaries
       *  if reached the end and circular is true then wejump to the first highlighted word 
       *  
       */  
        public function getPrevious( circular:Boolean=false,startIndex:int=-1):Range
        {
        	 var boundary:StringBoundary ;
             var len:int = this.boundariesToHighlight.length ;
             var caretIndex:int = (startIndex==-1 ?  this.textfield.caretIndex : startIndex) ;
            //search for first boundary from caretIndex
             for (  var i:int = len-1 ; i>= 0; i--)
             {
                 boundary = StringBoundary(this.boundariesToHighlight[i]); 
                 if (boundary.endIndex <= caretIndex)
                    break; 
             }
	         if (i==-1 && circular)
	         {
	         	  boundary = StringBoundary(this.boundariesToHighlight[len-1]);
	         }
	         else if (i==-1)
	             return null; 
	         return new Range(boundary.endIndex,boundary.beginIndex);
        }  
        /**
        * ignore the highlighted word, by removing the current highlight and never highlighting it again 
        * use only in highlightword mode
        */  
        public function ignoreHighlightedRange(range:Range):void
        {
        	 if (range ==null || range.beginIndex == range.endIndex)	 return;
             var len:int = this.boundariesToHighlight.length ;
             var boundary:StringBoundary ;
             var tr:TextRange ;
      
             for (  var i:int = len-1 ; i>= 0; i--)
             {
                 boundary = StringBoundary(this.boundariesToHighlight[i]);
                 if ((boundary.beginIndex == range.beginIndex && boundary.endIndex ==range.endIndex)
                  || (boundary.beginIndex == range.endIndex && boundary.endIndex == range.beginIndex))
                 {
                 	//remove the tag
		            tr = new TextRange (UIComponent(this.target),false,boundary.beginIndex,boundary.endIndex); 
            		tr.kerning = true; 
            		target.invalidateHighlights();
            		this.boundariesToHighlight.splice(i,1);//remove from the array
                	break;
                 }
             } 
        }
		
        /**
        * removes the highlighted range and invalidates the text field to cause a redraw.
        * Use only in Highlight range mode
        * */
        public function removeHighlightedRange(range:Range,deleteText:Boolean=false):void
        {
        	 if (range ==null || range.beginIndex == range.endIndex)	 return;
             var len:int = this.boundariesToHighlight.length ;
             var boundary:StringBoundary ;
             var tr:TextRange ;
      
             for (  var i:int = len-1 ; i>= 0; i--)
             {
                 boundary = StringBoundary(this.boundariesToHighlight[i]);
                 if ((boundary.beginIndex == range.beginIndex && boundary.endIndex ==range.endIndex)
                  || (boundary.beginIndex == range.endIndex && boundary.endIndex == range.beginIndex))
                 {
                 	//remove the tag
		            tr = new TextRange (UIComponent(this.target),false,boundary.beginIndex,boundary.endIndex); 
            		tr.url = ""  ; 
            		if (deleteText) 
					{	tr.text = "" ;
						target.invalidateHighlightsLocations();
					}
					else
						target.invalidateHighlights();
            		this.boundariesToHighlight.splice(i,1);//remove from the array
					 
                	break;
                 }
             } 
            
        }
         
        /**
        * Resets the Highlighter, clearing all data highlights and emptying the array of character boundaries.
        */
        public function reset():void
        {
           	//clear tags if used
           	this.words = null;
            this.boundariesToHighlight = []; //reset the array so we recalculate the positions
             //this.clearFormating(); 
           	if (this.useTagging && this.textfield.htmlText != null)
        	{ 
	        	var htmlStr:String = this.textfield.htmlText;
	        	var tagRegExp:RegExp = new RegExp("<a\\shref=\"event:key-" + key + "\"[^<>]*>([^<>]*)</a>","gi") ;
	        	htmlStr = htmlStr.replace(tagRegExp,"$1"); 
	        	textfield.htmlText = htmlStr; 
	        }
	        listen(false);
        }
            
       /***
       *  Returns range surrounding character index from the list of boundaries
       *  index : zero based index of character 
       */  
        public function getRange(index:int=-1):Range
        {
        	 var boundary:StringBoundary ;
             var len:int = this.boundariesToHighlight.length ;
             var caretIndex:int = (index==-1 ?  this.textfield.caretIndex : index) ;
            //search for first boundary from caretIndex
             for (  var i:int = len-1 ; i>= 0; i--)
             {
                 boundary = StringBoundary(this.boundariesToHighlight[i]);
                 if (boundary.endIndex >= caretIndex && boundary.beginIndex <= caretIndex)
                    return Range(boundary);
             }	
             return null;
        }
        
        private function listen(attach:Boolean=true):void
        {
          	if (!attachedEvents && attach)
			{   attachedEvents = true;
				try{
					this.target.removeEventListener(CustomTextArea.DRAW_EVENT,draw_Handler,false);
				   }catch(e:Error)
				{ }
				try{
					this.target.removeEventListener(CustomTextArea.REFRESH_EVENT,refresh_Handler,false);
				}catch(e:Error)
				{}
				this.target.addEventListener(CustomTextArea.DRAW_EVENT,draw_Handler,false,0,true);
            	this.target.addEventListener(CustomTextArea.REFRESH_EVENT,refresh_Handler,false,0,true);
   			}
   			else if (attachedEvents && !attach)
   			{   attachedEvents =false;
   				this.target.removeEventListener(CustomTextArea.DRAW_EVENT,draw_Handler,false)
            	this.target.removeEventListener(CustomTextArea.REFRESH_EVENT,refresh_Handler,false);
      		}	
        }
        
        /**
        * relocates words or tags, rebuilding the boundaries array
        */ 
        private function refresh_Handler(evt:Event):void
        {
            /*  if (target.id.indexOf("1") > 0   )
        	 	 trace ("refresh_Handler " + _key , id , boundariesToHighlight.length, "\n", this.target.htmlText );  */
    		if (useTagging)
        		locateTags(); //update boundaries from tags 
         	else
        	    locateWords(); // update boundaries by researching for words
        	
        	draw_Handler();
        	
        }
       
         /**  wraps the selected boundaries with a url event:key */
        private function tagBoundaries():void
        {   
        	useTagging = true;
        	var len:int = this.boundariesToHighlight.length; 
        	
        	/* working url tag wrapping */
        	// trace (target.htmlText);
        	 var tr:TextRange ;  
        	 for(var i:int=0; i<len; i++){
            	var str:StringBoundary = this.boundariesToHighlight[i];  
            	tr = new TextRange (UIComponent(this.target),false,str.beginIndex,str.endIndex);
            	 
           /*  	 tr.text =  tr.text.replace(/[\r\n]/g,"__|__"); 
            	 tr.htmlText =  tr.htmlText.replace(/__\|__/g,"<BR>"); */
            	 tr.url = "event:key-" + key  ; 
             }
         }
	
		
		/**
		* Sets a custom pattern for the underline effect. This can be a display object or bitmapdata object to use as the repeating underline pattern.
		* This is drawn into a new BitmapData instance for internal use, and is always returned as a BitmapData object.
		**/
		
		private function  getUnderlinePattern(color:uint):BitmapData 
		{   
			if (_underlinePattern != null) return _underlinePattern;
		    var width:int = 4;
		    var height:int = 3;
			_underlinePattern  = new BitmapData(width,height,true, 0xFFFFFFFF);
			/* _underlinePattern.fillRect(new Rectangle(0,0,1,1),0xFF000000);//black
			_underlinePattern.fillRect(new Rectangle(1,1,1,1),0xFF0000FF);//blue
			_underlinePattern.fillRect(new Rectangle(2,2,1,1),0xFF00FF00);//grean
			_underlinePattern.fillRect(new Rectangle(3,1,1,1),0xFFFF0000);//red
			 */
			var y:int;
			for (var x:int=0; x<width; x++)
			{  
				y = ((x%4)==3 ? 1 : x%4) ;
				//trace (x , y ) ;
				//_underlinePattern.fillRect(new Rectangle(x,y,1,1),color);
				 _underlinePattern.setPixel32(x,y,color);
			}  
			return _underlinePattern;
		}
		 
		
        /**
         * draws rectangles on the target sprite
        */
        private function draw_Handler(evt:Event=null):void
        { 
        	var len:int = this.boundariesToHighlight.length; 
         	//if (target.id.indexOf("1") > 0   )
        	 	//trace ("draw highlights " + _key , id , boundariesToHighlight.length, "\n", this.target.htmlText );
            for(var i:int=0; i<len; i++)
            {
            	var str:StringBoundary = this.boundariesToHighlight[i];  
            	if(str.isVisible)
            	{
            		var rects:Array = str.rectangles;
            		var len2:int = rects.length;
            		for(var j:int=0; j<len2; j++)
            		{
            			var rect:Rectangle = rects[j];
            			if (this.textFormat.highlight)
            			{ 
			            	fillRect(rect,this.textFormat.highlightColor);
			            }
			            if (this.textFormat.underlinePatterned)
			            { 
							//var baselineY:Number = rect.y+rect.height+2; // 2 for gutter + 1 to offset
							var rect2:Rectangle = new Rectangle(rect.x,rect.bottom-1,rect.width,3);  
							fillRectPattern(rect2,getUnderlinePattern(this.textFormat.underlinePatternedColor));
			            }
			            if (this.textFormat.strikethrough)
			            {
			                fillRect(new Rectangle(rect.x,rect.y+(rect.height/2),rect.width,2),this.textFormat.strikethroughColor);
			            }
            		}
            	}
             
                   
            } 
        
        } 
        
        /** draws a rectangle */
        private function fillRect(rect:Rectangle,color:uint):void {
        	target.highlightsCanvas.graphics.beginFill(color,1);
        	target.highlightsCanvas.graphics.drawRect(rect.x,rect.y,rect.width,rect.height);
        	target.highlightsCanvas.graphics.endFill();
			/* highlightsCanvas.graphics.beginFil
			highlightsCanvas.graphics.drawRoundRect(x,y+height-ascent,width,ascent+descent,6); */
		}
		
		private function fillRectPattern(rect:Rectangle,pattern:BitmapData):void {
			target.highlightsCanvas.graphics.beginBitmapFill(pattern,new Matrix(1,0,0,1,1,1),true,false);
        	target.highlightsCanvas.graphics.drawRect(rect.x,rect.y,rect.width,rect.height);
        	target.highlightsCanvas.graphics.endFill();
			/* highlightsCanvas.graphics.beginFill(_color,(_color>>>24)/0xFF);
			highlightsCanvas.graphics.drawRoundRect(x,y+height-ascent,width,ascent+descent,6); */
		}
		
        
        /**locate the list of words in words Array and generates their boundaries **/
        private function locateWords():void
        {
            var txt:String  = this.textfield.text;
			var position:int=-1;
            var wordPattern:RegExp ; 
            target.validateNow();
            this.boundariesToHighlight = [];
             var registerMatch:Function = function (matchedSubstring:String, 
                                 index:int, 
                                 str:String):String
            {
            	if (target != null && textfield != null)
            	{
            		var tr:TextRange = new TextRange (UIComponent(target),false,index,index+matchedSubstring.length);
					if (!tr.kerning && tr.url.length ==0)
            			boundariesToHighlight.push(new StringBoundary(textfield,index,index+matchedSubstring.length,xOffset,yOffset));
            	}
            /* 	else 
           			boundariesToHighlight.push(new StringBoundary(textfield,index,index+matchedSubstring.length,xOffset,yOffset));
            	 */
            	return matchedSubstring;
            }
            
			for each (var word:String in words)
			{ 
				wordPattern = new RegExp( "\\b"+ word +"\\b" ,(caseSensitive ? "ig":"g"));
				txt = txt.replace(wordPattern,registerMatch);
			}
        
        } 
        
          /**
		  update the boundaries array from possibly changed possitions of the tagedboundaries using the tags. 
		*/
        private function locateTags():void
        {
        	if (this.textfield ==null || this.textfield.htmlText==null) return;
        	target.validateNow(); 
			this.boundariesToHighlight = [];	
        	//basically we need to find the url tag location and get its textual indexes. 
        	var htmlText:String= this.textfield.htmlText;
        //	trace (this.target.id + "_" + this._key +"_"+ this.id + " Finding Tags: ",htmlText);
            var tagRegExp:RegExp = new RegExp("<a\\shref=\"event:key-" + key + "\"[^<>]*>([^<>]*)</a>(?:</font>(</p>))?","gi") ;  
            var position:int;
            var registerMatch:Function = function (matchedSubstring:String, arg:String,arg2:String,
                                 index:int, 
                                 str:String):String
            {
            	var lastBoundary:StringBoundary ;
            	var beginTextIndex:int = calculateTextIndex(str,index);
            	//var endTextIndex2:int = calculateTextIndex(str,index + matchedSubstring.length-1)+1; 
            	var endTextIndex:int = beginTextIndex + arg.length + (arg2!= ""? 1:0);
            	//trace ("index ",index,  " begin " , beginTextIndex, " end " ,endTextIndex , "end2" ,endTextIndex2); 
        		if (boundariesToHighlight.length>0)
        		{ //if there are already boundaries get last boundary 
        	      lastBoundary=  StringBoundary(boundariesToHighlight[boundariesToHighlight.length-1]);
        	 	}
        	 	if ((lastBoundary !=null) && (lastBoundary.endIndex== beginTextIndex))
        	 	{//continuation of last text as in the case of new line
        	 	   lastBoundary.endIndex = endTextIndex;
            	}
            	else
        	 	{  
            	    boundariesToHighlight.push(new StringBoundary(textfield,beginTextIndex,endTextIndex,xOffset,yOffset));
            	} 
            	return matchedSubstring;
            
            }
            
            htmlText.replace(tagRegExp,registerMatch);
        }
        
        
        /* private function clearFormating():void
        {
            // clear formatings
            var len:int = this.boundariesToHighlight.length;
            for(var i:int=0; i<len; i++){
          	    var str:StringBoundaries = this.boundariesToHighlight[i]; 	
            	textfield.setTextFormat(textfield.defaultTextFormat,str.beginIndex, str.endIndex); 
            } 
        } */
        
      
		
		/**Returns the zero text index of the zero based html index specified*/
		private  function calculateTextIndex(htmlStr:String, htmlIndex:int):int
		{
		   var htmlCounter:int = 0; //length of the html passed or 1 based htmlindex
		   var textIndex:int = 0;//zero based text index
		   //"0<1a2b3c4d5e6f7g8h9i10>11A12h13&14g15t16;17a18d19<20d21d22d23>24
		   //0A1h2>    3a4d5"
		   //the following entities have size zero, the rest have size one.
		   var sizeOneEntities:String = "|&lt;|&gt;|&amp;|&quot;|&apos;|<BR>|</BR>|</P>|";
		   
		   // characters that appears when a tag starts
		   var openTags:String = "<&";
		   // characters that appears when a tag ends
		  // var closeTags:String = ">;";
		   
		    /**generates an array of tokens which are tags, entities and text. */
		      var tagPattern:RegExp = /<[^<>\s\/]*\s*[^<>]*[^P]?>|&(?:lt|gt|amp|quot|apos);|[^<>&]*/g;
		    var tokensArray:Array = htmlStr.match(tagPattern); 
		    var i:int = 0;
		    var currentToken:String;
		    var currentTokenLen:int;
		    var isHtmlToken:Boolean ;
		    while ((htmlCounter <  htmlIndex) && (i < tokensArray.length))
		    {
		    	 currentToken = tokensArray[i++];
		    	 currentTokenLen = currentToken.length;
		    	 isHtmlToken = (openTags.indexOf(currentToken.charAt(0)) != -1)  ;
		    	 if (isHtmlToken)  //this token is HMTL or its a string begining with & and ending with ; and is shorter than 4 chars
		    	 {//whole tag. 
		    	    //this html corresponds to 1 textual character that is counted only if it is fully included in the htmlindex
		    	     if ((sizeOneEntities.indexOf("|" + currentToken + "|") != -1)
		    	    && (htmlCounter + currentTokenLen) <= htmlIndex
		    	    ) 
		    	       textIndex++; 
		    	    htmlCounter += currentTokenLen;
		    	 }
		    	 else if ((htmlCounter + currentTokenLen) > htmlIndex)
		    	 {  //the text index is inside the current token which is a text
		    	    textIndex += htmlIndex - htmlCounter;
		    	 	htmlCounter += currentTokenLen;
		    	 	
		    	 	break;
		    	 }
		    	 else  //add the text size to html and text counters and move on
		    	 {
		    	 	 textIndex += currentTokenLen;
		    	 	 htmlCounter += currentTokenLen;
		    	 	 
		    	 }  
		    }
		    return textIndex;
		}  
 		
    }
}

